import os
import sys
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import PyInstaller.__main__

# PackageTool类：Python程序打包工具的主类
# 功能：提供图形界面，允许用户选择Python文件并使用PyInstaller将其打包成可执行文件
# 特点：
# 1. 支持单文件打包
# 2. 可选择是否显示控制台
# 3. 支持自定义图标
# 4. 自动清理临时文件
# 5. 支持打包时添加等待退出功能
class PackageTool:
    def __init__(self):
        # 初始化主窗口
        self.window = tk.Tk()
        self.window.title("Python程序打包工具")
        self.window.geometry("600x400")  # 设置窗口大小
        
        # 创建主框架，用于容纳所有控件
        self.main_frame = ttk.Frame(self.window, padding="10")
        self.main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 文件选择部分
        ttk.Label(self.main_frame, text="Python文件:").grid(row=0, column=0, sticky=tk.W)
        self.file_path = tk.StringVar()  # 存储选择的Python文件路径
        ttk.Entry(self.main_frame, textvariable=self.file_path, width=50).grid(row=0, column=1, padx=5)
        ttk.Button(self.main_frame, text="浏览", command=self.select_file).grid(row=0, column=2)
        
        # 输出文件名称设置
        ttk.Label(self.main_frame, text="输出名称:").grid(row=1, column=0, sticky=tk.W, pady=10)
        self.output_name = tk.StringVar()  # 存储输出文件名
        ttk.Entry(self.main_frame, textvariable=self.output_name, width=50).grid(row=1, column=1, padx=5, pady=10)
        
        # 打包选项框架
        option_frame = ttk.LabelFrame(self.main_frame, text="打包选项", padding="10")
        option_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
        
        # 打包选项设置
        self.onefile = tk.BooleanVar(value=True)  # 是否打包成单个文件
        self.noconsole = tk.BooleanVar(value=False)  # 是否显示控制台
        self.clean = tk.BooleanVar(value=True)  # 是否清理临时文件
        
        # 打包选项复选框
        ttk.Checkbutton(option_frame, text="打包成单个文件", variable=self.onefile).grid(row=0, column=0, sticky=tk.W)
        ttk.Checkbutton(option_frame, text="不显示控制台", variable=self.noconsole).grid(row=0, column=1, sticky=tk.W)
        ttk.Checkbutton(option_frame, text="清理临时文件", variable=self.clean).grid(row=0, column=2, sticky=tk.W)
        
        # 图标文件选择
        ttk.Label(self.main_frame, text="图标文件:").grid(row=3, column=0, sticky=tk.W)
        self.icon_path = tk.StringVar()  # 存储图标文件路径
        ttk.Entry(self.main_frame, textvariable=self.icon_path, width=50).grid(row=3, column=1, padx=5)
        ttk.Button(self.main_frame, text="浏览", command=self.select_icon).grid(row=3, column=2)
        
        # 状态显示标签
        self.status = tk.StringVar()  # 存储当前状态信息
        ttk.Label(self.main_frame, textvariable=self.status).grid(row=4, column=0, columnspan=3, pady=10)
        
        # 打包按钮
        ttk.Button(self.main_frame, text="开始打包", command=self.build_exe).grid(row=5, column=0, columnspan=3, pady=10)
    
    def select_file(self):
        """选择要打包的Python文件
        功能：打开文件选择对话框，让用户选择要打包的Python文件
        选择文件后会自动设置输出文件名（不包含扩展名）
        """
        file = filedialog.askopenfilename(filetypes=[("Python Files", "*.py")])
        if file:
            self.file_path.set(file)
            # 自动设置输出名称为文件名（不含扩展名）
            default_name = os.path.splitext(os.path.basename(file))[0]
            self.output_name.set(default_name)
    
    def select_icon(self):
        """选择可执行文件的图标
        功能：打开文件选择对话框，让用户选择.ico格式的图标文件
        """
        icon = filedialog.askopenfilename(filetypes=[("Icon Files", "*.ico")])
        if icon:
            self.icon_path.set(icon)
    
    def build_exe(self):
        """执行打包操作
        功能：将选择的Python文件打包成可执行文件
        处理流程：
        1. 检查输入文件
        2. 检查并清理目标文件
        3. 备份源文件
        4. 修改源文件（添加等待退出功能）
        5. 执行打包
        6. 还原源文件
        7. 清理临时文件
        """
        # 检查是否选择了Python文件
        if not self.file_path.get():
            messagebox.showerror("错误", "请选择要打包的Python文件！")
            return
        
        # 检查目标文件是否被占用
        output_exe = os.path.join(os.getcwd(), 'dist', f'{self.output_name.get()}.exe')
        if os.path.exists(output_exe):
            try:
                os.chmod(output_exe, 0o777)  # 修改文件权限
                os.remove(output_exe)  # 删除已存在的文件
            except Exception as e:
                messagebox.showerror("错误", f"目标文件被占用，请关闭所有相关程序后重试：{str(e)}")
                return
        
        # 定义备份文件路径
        backup_file = self.file_path.get() + '.bak'
        content = ""
        
        # 检查是否在打包工具自身
        is_self_packaging = os.path.abspath(self.file_path.get()) == os.path.abspath(__file__)
        
        # 设置PyInstaller打包参数
        params = [
            self.file_path.get(),
            f'--name={self.output_name.get() or "output"}',
            '--hidden-import=tkinter',
            '--hidden-import=PyInstaller',
            '--collect-all=tkinter',
            f'--add-binary={os.path.join(os.path.dirname(sys.executable), "python312.dll")};.',
            f'--add-binary={os.path.join(os.path.dirname(sys.executable), "vcruntime140.dll")};.',
            '--runtime-tmpdir=.'
        ]
        
        try:
            # 更新状态显示
            self.status.set("正在打包中...")
            self.window.update()
            
            # 清理旧的构建文件
            self._clean_old_files()
            
            # 如果不是打包工具自身，则修改源文件添加等待退出功能
            if not is_self_packaging:
                try:
                    # 读取原始文件内容
                    with open(self.file_path.get(), 'r', encoding='utf-8') as f:
                        content = f.read()
                    # 备份原文件
                    with open(backup_file, 'w', encoding='utf-8') as f:
                        f.write(content)
                    # 修改源文件，添加等待退出功能
                    with open(self.file_path.get(), 'w', encoding='utf-8') as f:
                        if 'if __name__ == "__main__":' in content:
                            # 如果存在main块，在其中添加等待退出功能
                            main_index = content.find('if __name__ == "__main__":')  # 找到main块位置
                            main_block = content[main_index:]  # 提取main块内容
                            main_content = main_block[main_block.find('\n')+1:].strip()  # 获取main块中的代码内容
                            
                            # 写入原始内容直到main块
                            f.write(content[:main_index])
                            
                            # 添加带有等待退出功能的新main块
                            f.write('if __name__ == "__main__":\n')
                            f.write('    try:\n')
                            f.write('        ' + main_content + '\n')
                            f.write('    except Exception as e:\n')
                            f.write('        print(f"程序运行出错: {str(e)}")\n')  # 添加换行符
                            f.write('    finally:\n')
                            f.write('        print("\\n程序执行完成，按任意键退出...", end="")\n')
                            f.write('        sys.stdout.flush()\n')
                            f.write('        try:\n')
                            f.write('            import msvcrt\n')
                            f.write('            msvcrt.getch()\n')
                            f.write('        except ImportError:\n')
                            f.write('            input()\n')
                        else:
                            # 如果不存在main块，保持原始内容不变
                            f.write(content)
                except Exception as e:
                    messagebox.showerror("错误", f"修改源文件时出错：{str(e)}")
                    return
            
            # 根据用户选择添加打包选项
            if self.onefile.get():
                params.append('--onefile')
            if self.noconsole.get():
                params.append('--noconsole')
            if self.clean.get():
                params.append('--clean')
            if self.icon_path.get():
                params.append(f'--icon={self.icon_path.get()}')
            
            # 执行打包操作
            PyInstaller.__main__.run(params)
            
            # 还原原始文件
            if not is_self_packaging and os.path.exists(backup_file):
                os.replace(backup_file, self.file_path.get())
            
            # 清理临时文件
            self._clean_temp_files()
            
            # 更新状态并显示成功消息
            self.status.set("打包完成！")
            messagebox.showinfo("成功", f"打包完成！\n文件保存在: {os.path.join(os.getcwd(), 'dist', self.output_name.get())}.exe")
        except Exception as e:
            # 打包失败处理
            self.status.set("打包失败！")
            messagebox.showerror("错误", f"打包过程中出现错误：{str(e)}")
        finally:
            # 确保在任何情况下都还原原始文件
            if not is_self_packaging and os.path.exists(backup_file):
                os.replace(backup_file, self.file_path.get())
    
    def _clean_old_files(self):
        """清理旧的构建文件
        功能：删除上次构建产生的临时文件和目录
        清理对象：
        1. build目录
        2. dist目录
        3. __pycache__目录
        4. .spec文件
        """
        dirs_to_clean = ['build', 'dist', '__pycache__']
        files_to_clean = [f'{self.output_name.get()}.spec']
        
        # 清理目录
        for dir_name in dirs_to_clean:
            if os.path.exists(dir_name):
                try:
                    import shutil
                    # 确保文件夹中的文件都可以被删除
                    for root, dirs, files in os.walk(dir_name):
                        for file in files:
                            file_path = os.path.join(root, file)
                            try:
                                os.chmod(file_path, 0o777)  # 修改文件权限确保可以删除
                            except:
                                pass
                    shutil.rmtree(dir_name, ignore_errors=True)
                except Exception as e:
                    print(f"清理 {dir_name} 时出错: {str(e)}")
        
        # 清理文件
        for file_name in files_to_clean:
            if os.path.exists(file_name):
                try:
                    os.chmod(file_name, 0o777)  # 修改
                    os.remove(file_name)
                except Exception as e:
                    print(f"删除 {file_name} 时出错: {str(e)}")
    
    def _clean_temp_files(self):
        """清理临时文件"""
        if os.path.exists('build'):
            import shutil
            shutil.rmtree('build')
        
        spec_file = f'{self.output_name.get()}.spec'
        if os.path.exists(spec_file):
            os.remove(spec_file)
    
    def run(self):
        self.window.mainloop()

if __name__ == "__main__":
    tool = PackageTool()
    tool.run()